package lexer;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import symbols.*;
import tools.BasicTools;

public class Lexer {
    /** 行号 */
    private int line = 1;
    /** 关键字表 */
    private HashMap<String, Word> keywords = new HashMap<>();
    /** 待处理的源代码 */
    private File file;
    private String code;
    private StringReader sr;
    /** 保存Token的列表 */
    private ArrayList<Token> tokens = new ArrayList<>();
    /** 当前读取的字符 */
    private int peek = 0;

    public static void main(String[] args) throws IOException {
        Lexer lexer = new Lexer(args[0]);
        /** input test.i */
        lexer.readCode();
        System.out.println("<Lexer>读取代码:\n" + lexer.getCode());
        /** scan */
        List<Token> t = lexer.scan();
        System.out.println("<Lexer>词法分析结果:");
        for (Token token : t) {
            System.out.printf("%s\ttag=%03X%n", token, token.tag);
        }
        /** output test.csv */
        lexer.writeCSV();
        System.out.println("<Lexer>已成功写出词法单元序列.");
    }

    public Lexer(String pathName) {
        file = new File(pathName);
        // 保留字
        keywords.put(Type._int.lexeme, Type._int);
        keywords.put(Word._if.lexeme, Word._if);
        keywords.put(Word._else.lexeme, Word._else);
        keywords.put(Word._while.lexeme, Word._while);
        keywords.put(Word.ge.lexeme, Word.ge);
        keywords.put(Word.le.lexeme, Word.le);
        keywords.put(Word.eq.lexeme, Word.eq);
        keywords.put(Word.ne.lexeme, Word.ne);
        keywords.put(Word.minus.lexeme, Word.minus);
    }

    public String getCode() {
        return code;
    }

    public List<Token> getTokens() {
        return tokens;
    }

    public void readCode() throws IOException {
        code = BasicTools.readFromFile(file);
    }

    public void readCode(String code) {
        this.code = code;
    }

    public List<Token> scan() throws IOException {
        sr = new StringReader(code);
        for (readChar(); peek != -1;) {
            if (peek == '\n') {// 换行符
                line++;
                readChar();
            } else if (peek == '\r' || peek == ' ' || peek == '\t') {// 排版符号
                readChar();
            } else if (Character.isDigit(peek)) {// 整常数
                readNum();
            } else if (Character.isLetter(peek) || peek == '_') {// 名字
                readWord();
            } else {
                readOther();
            }
        }
        return tokens;
    }

    private void readChar() throws IOException {
        peek = sr.read();
    }

    private void readNum() throws IOException {
        int count = 0;
        StringBuilder sb = new StringBuilder();
        do {
            count++;
            sb.append((char) peek);
            readChar();
        } while (Character.isDigit(peek));
        String s = sb.toString();
        if (count > 15) {
            throw new IOException(
                    "错误: line " + line + ": 常整数长度不能超过15: '" + s + "'");
        }
        if (count > 1 && s.charAt(0) == '0') {
            throw new IOException("错误: line " + line + ": 非零常整数的第一个字符不能为0: '" + s + "'");
        }
        try {
            tokens.add(new Num(Integer.parseInt(s)));
        } catch (NumberFormatException e) {// 存在Integer.MIN_VALUE报错问题
            throw new IOException("错误: line " + line + ": 超出了整形范围: '" + s + "'");
        }
    }

    private void readWord() throws IOException {
        int count = 0;
        StringBuilder sb = new StringBuilder();
        do {
            count++;
            sb.append((char) peek);
            readChar();
        } while (Character.isLetterOrDigit(peek) || peek == '_');
        String s = sb.toString();
        if (count > 15) {
            throw new IOException(
                    "错误: line " + line + ": 标识符长度不能超过15: '" + s + "'");
        }
        Word w;
        if (keywords.containsKey(s)) {
            w = keywords.get(s);
        } else {
            w = new Word(s, Tag.ID);
            keywords.put(s, w);
        }
        tokens.add(w);
    }

    private void readOther() throws IOException {
        switch (peek) {
            case '>':
                readChar();
                if (peek == '=') {
                    tokens.add(Word.ge);
                    readChar();
                } else {
                    tokens.add(new Token('>'));
                }
                break;
            case '<':
                readChar();
                if (peek == '=') {
                    tokens.add(Word.le);
                    readChar();
                } else {
                    tokens.add(new Token('<'));
                }
                break;
            case '=':
                readChar();
                if (peek == '=') {
                    tokens.add(Word.eq);
                    readChar();
                } else {
                    tokens.add(new Token('='));
                }
                break;
            case '!':
                readChar();
                if (peek == '=') {
                    tokens.add(Word.ne);
                    readChar();
                } else {
                    tokens.add(new Token('!'));
                }
                break;
            case '{':
            case '}':
            case '(':
            case ')':
            case ',':
            case ';':
            case '+':
            case '-':
            case '*':
            case '/':
                tokens.add(new Token(peek));
                readChar();
                break;
            default:
                throw new IOException(
                        "错误: line " + line + ": 非法字符: '" + ((peek < 256) ? (char) peek : ("\\" + peek)) + "'");
        }
    }

    private void writeCSV() throws IOException {
        StringBuilder sb = new StringBuilder();
        for (Token token : tokens) {
            sb.append(token.tag + "," + (token.toString().equals(",") ? "\",\"" : token) + "\n");
        }
        BasicTools.WriteToFile(BasicTools.replaceFileSuffix(file, ".csv"), sb.toString());
    }
}
